package com.ikingtech.platform.service.application.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ikingtech.framework.sdk.approve.api.ApproveFormInstanceApi;
import com.ikingtech.framework.sdk.approve.embedded.ApproveOps;
import com.ikingtech.framework.sdk.approve.model.ApproveFormInstanceDTO;
import com.ikingtech.framework.sdk.approve.model.ApproveFormInstancePreviewParamDTO;
import com.ikingtech.framework.sdk.base.model.PageResult;
import com.ikingtech.framework.sdk.context.exception.FrameworkException;
import com.ikingtech.framework.sdk.context.security.Me;
import com.ikingtech.framework.sdk.core.response.R;
import com.ikingtech.framework.sdk.enums.application.ApplicationPageTypeEnum;
import com.ikingtech.framework.sdk.enums.approve.ApproveFormInstancePreviewTypeEnum;
import com.ikingtech.framework.sdk.enums.approve.ApproveFormTypeEnum;
import com.ikingtech.framework.sdk.utils.Tools;
import com.ikingtech.platform.service.application.entity.*;
import com.ikingtech.platform.service.application.exception.ApplicationExceptionInfo;
import com.ikingtech.platform.service.application.service.*;
import com.ikingtech.platform.service.application.entity.*;
import com.ikingtech.platform.service.application.service.*;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

/**
 * @author tie yan
 */
@RequiredArgsConstructor
@RestController
@RequestMapping(value = "/application/general-data")
public class ApplicationGeneralDataController {

    private final ApplicationGeneralDataService service;

    private final ApplicationPageService pageService;

    private final ApplicationPageModelService pageModelService;

    private final ApplicationModelService modelService;

    private final ApplicationModelRelationService modelRelationService;

    private final ApproveFormInstanceApi approveFormInstanceApi;

    private final SqlHelper sqlHelper;

    @PostMapping("/add")
    public R<Object> add(@RequestParam String page,
                         @RequestParam(required = false) String businessCode,
                         @RequestBody Object data) {
        ApplicationPageDO pageEntity = this.pageService.getById(page);
        List<ApplicationPageModelDO> pageModelEntities = null == pageEntity ? new ArrayList<>() : this.pageModelService.listByPageId(pageEntity.getId());
        Map<String, Object> dataMap = Tools.Json.objToMap(data);
        this.setCommonField(dataMap);
        if (Tools.Coll.isNotBlank(pageModelEntities)) {
            this.dealWithModelPage(
                    pageModelEntities,
                    modelEntity -> this.sqlHelper.save(modelEntity.getCode(), dataMap),
                    (modelEntity, slaveModelEntity) -> {
                        Object slaveModelData = dataMap.get(slaveModelEntity.getCode() + "List");
                        Map<String, Object> slaveModelDataMap = Tools.Json.objToMap(slaveModelData);
                        this.setCommonField(slaveModelDataMap);
                        slaveModelDataMap.put(modelEntity.getCode() + "Id", dataMap.get("id"));
                        this.sqlHelper.save(slaveModelEntity.getCode(), slaveModelDataMap);
                        dataMap.remove(slaveModelEntity.getCode() + "List");
                    }
            );
        } else {
            ApplicationGeneralDataDO entity = new ApplicationGeneralDataDO();
            entity.setId((String) dataMap.get("id"));
            entity.setBusinessCode(businessCode);
            entity.setData(Tools.Json.toJsonStr(data));
            entity.setPageId(page);
            this.service.save(entity);
        }
        if (null != pageEntity && ApplicationPageTypeEnum.APPROVE.name().equals(pageEntity.getType())) {
            ApproveFormInstancePreviewParamDTO previewParam = new ApproveFormInstancePreviewParamDTO();
            previewParam.setPreviewType(ApproveFormInstancePreviewTypeEnum.BY_FORM_CONFIG);
            previewParam.setFormType(ApproveFormTypeEnum.PROCESS_ONLY);
            previewParam.setFormBusinessType(pageEntity.getName());
            String formData = Tools.Json.toJsonStr(data);
            previewParam.setFormData(formData);
            ApproveFormInstanceDTO formInstance = this.approveFormInstanceApi.preview(previewParam).getData();
            formInstance.setBusinessDataId((String) dataMap.get("id"));
            formInstance.setFormData(formData);
            ApproveOps.submit(formInstance);
        }
        return R.ok();
    }

    @PostMapping("/delete")
    public R<Object> delete(@RequestParam String page,
                            @RequestBody String id) {
        ApplicationPageDO pageEntity = this.pageService.getById(page);
        List<ApplicationPageModelDO> pageModelEntities = null == pageEntity ? new ArrayList<>() : this.pageModelService.listByPageId(pageEntity.getId());
        if (Tools.Coll.isNotBlank(pageModelEntities)) {
            this.dealWithModelPage(
                    pageModelEntities,
                    modelEntity -> this.sqlHelper.removeById(modelEntity.getCode(), id),
                    (modelEntity, slaveModelEntity) ->
                            this.sqlHelper.removeBy(slaveModelEntity.getCode(), modelEntity.getCode() + "Id", id)
            );
        } else {
            this.service.removeById(id);
        }
        return R.ok();
    }

    @PostMapping("/update")
    public R<Object> update(@RequestParam String page,
                            @RequestBody Object data) {
        ApplicationPageDO pageEntity = this.pageService.getById(page);
        List<ApplicationPageModelDO> pageModelEntities = null == pageEntity ? new ArrayList<>() : this.pageModelService.listByPageId(pageEntity.getId());
        Map<String, Object> dataMap = Tools.Json.objToMap(data);
        String masterModelId = (String) dataMap.get("id");
        this.setCommonField(dataMap);
        if (Tools.Coll.isNotBlank(pageModelEntities)) {
            this.dealWithModelPage(
                    pageModelEntities,
                    modelEntity -> this.sqlHelper.updateById(modelEntity.getCode(), dataMap),
                    (modelEntity, slaveModelEntity) -> {
                        Object slaveModelData = dataMap.get(slaveModelEntity.getCode() + "List");
                        Map<String, Object> slaveModelDataMap = Tools.Json.objToMap(slaveModelData);
                        this.setCommonField(slaveModelDataMap);
                        slaveModelDataMap.put(modelEntity.getCode() + "Id", masterModelId);
                        this.sqlHelper.removeBy(slaveModelEntity.getCode(), modelEntity.getCode() + "Id", masterModelId);
                        this.sqlHelper.save(slaveModelEntity.getCode(), slaveModelDataMap);
                        dataMap.remove(slaveModelEntity.getCode() + "List");
                    }
            );
        } else {
            ApplicationGeneralDataDO entity = this.service.getById(masterModelId);
            entity.setData(Tools.Json.toJsonStr(data));
            this.service.updateById(entity);
        }
        return R.ok();
    }

    @PostMapping("/list/page")
    public R<List<Map<String, Object>>> page(@RequestParam String page,
                                             @RequestBody Object data) {
        ApplicationPageDO pageEntity = this.pageService.getById(page);
        List<ApplicationPageModelDO> pageModelEntities = null == pageEntity ? new ArrayList<>() : this.pageModelService.listByPageId(pageEntity.getId());
        Map<String, Object> dataMap = Tools.Json.objToMap(data);
        if (Tools.Coll.isNotBlank(pageModelEntities)) {
            ApplicationModelDO modelEntity = this.modelService.getById(pageModelEntities.get(0).getModelId());
            if (null == modelEntity) {
                throw new FrameworkException(ApplicationExceptionInfo.MODEL_NOT_FOUND);
            }
            return R.ok(this.sqlHelper.page(modelEntity.getCode(), dataMap));
        } else {
            LambdaQueryWrapper<ApplicationGeneralDataDO> queryWrapper = Wrappers.<ApplicationGeneralDataDO>lambdaQuery()
                    .eq(ApplicationGeneralDataDO::getPageId, page);
            dataMap.forEach((key, value) -> {
                if (!"page".equals(key) && !"rows".equals(key) && null != value) {
                    if (value instanceof List) {
                        queryWrapper.apply("JSON_CONTAINS(data, JSON_OBJECT('" + key + "', {0}))", value);
                    } else {
                        queryWrapper.apply("data -> '$." + key + "' LIKE CONCAT('%', {0}, '%')", value);
                    }
                }
            });
            Page<ApplicationGeneralDataDO> pageResult = this.service.page(new Page<>((Integer) dataMap.get("page"), (Integer) dataMap.get("rows")), queryWrapper);
            return R.ok(PageResult.build(pageResult.getPages(),
                    pageResult.getTotal(),
                    Tools.Coll.convertList(pageResult.getRecords(), this::generalDataConvert)));
        }
    }

    @PostMapping("/detail")
    public R<Map<String, Object>> detail(@RequestParam String page,
                                         @RequestBody String id) {
        ApplicationPageDO pageEntity = this.pageService.getById(page);
        List<ApplicationPageModelDO> pageModelEntities = null == pageEntity ? new ArrayList<>() : this.pageModelService.listByPageId(pageEntity.getId());
        if (Tools.Coll.isNotBlank(pageModelEntities)) {
            ApplicationModelDO modelEntity = this.modelService.getById(pageModelEntities.get(0).getModelId());
            if (null == modelEntity) {
                throw new FrameworkException(ApplicationExceptionInfo.MODEL_NOT_FOUND);
            }
            return R.ok(this.sqlHelper.getById(modelEntity.getCode(), id));
        } else {
            return R.ok(this.generalDataConvert(this.service.getById(id)));
        }
    }

    @PostMapping("/list/all")
    public R<List<Map<String, Object>>> all(@RequestParam String page,
                                            @RequestParam(required = false) String businessCode) {
        ApplicationPageDO pageEntity = this.pageService.getById(page);
        List<ApplicationPageModelDO> pageModelEntities = this.pageModelService.listByPageId(pageEntity.getId());
        if (Tools.Coll.isNotBlank(pageModelEntities)) {
            ApplicationModelDO modelEntity = this.modelService.getById(pageModelEntities.get(0).getModelId());
            if (null == modelEntity) {
                throw new FrameworkException(ApplicationExceptionInfo.MODEL_NOT_FOUND);
            }
            return R.ok(this.sqlHelper.list(modelEntity.getCode()));
        } else {
            return R.ok(Tools.Coll.convertList(this.service.listByPageIdAndBusinessCode(page, businessCode), this::generalDataConvert));
        }
    }

    private Map<String, Object> generalDataConvert(ApplicationGeneralDataDO entity) {
        Map<String, Object> rowData = Tools.Json.toMap(entity.getData());
        rowData.put("id", entity.getId());
        rowData.put("createBy", entity.getCreateBy());
        rowData.put("createName", entity.getCreateName());
        rowData.put("createTime", entity.getCreateTime());
        rowData.put("updateBy", entity.getUpdateBy());
        rowData.put("updateName", entity.getUpdateName());
        rowData.put("updateTime", entity.getUpdateTime());
        return rowData;
    }

    private void setCommonField(Map<String, Object> data) {
        if (!data.containsKey("id")) {
            data.put("id", Tools.Id.uuid());
            data.put("createBy", Me.id());
            data.put("createName", Me.username());
            data.put("createTime", Tools.DateTime.Formatter.simple());
        }
        data.put("updateBy", Me.id());
        data.put("updateName", Me.username());
        data.put("updateTime", Tools.DateTime.Formatter.simple());
    }

    private void dealWithModelPage(List<ApplicationPageModelDO> pageModelEntities, Consumer<ApplicationModelDO> masterModelDealer, BiConsumer<ApplicationModelDO, ApplicationModelDO> slaveModelDealer) {
        if (pageModelEntities.size() > 1) {
            throw new FrameworkException("notSupported");
        }
        ApplicationModelDO modelEntity = this.modelService.getById(pageModelEntities.get(0).getModelId());
        if (null == modelEntity) {
            throw new FrameworkException(ApplicationExceptionInfo.MODEL_NOT_FOUND);
        }
        List<ApplicationModelRelationDO> modelRelationEntities = this.modelRelationService.listByMasterModelId(modelEntity.getId());
        if (Tools.Coll.isNotBlank(modelRelationEntities)) {
            List<ApplicationModelDO> slaveModelEntities = this.modelService.listByIds(Tools.Coll.convertList(modelRelationEntities, ApplicationModelRelationDO::getModelId));
            for (ApplicationModelDO slaveModelEntity : slaveModelEntities) {
                slaveModelDealer.accept(modelEntity, slaveModelEntity);
            }
        }
        masterModelDealer.accept(modelEntity);
    }

    @RequiredArgsConstructor
    public static class SqlHelper {

        private final JdbcTemplate jdbcTemplate;

        private static final String INSERT_SQL_TEMPLATE = "INSERT INTO {} ({}) VALUES ({})";

        private static final String DELETE_SQL_TEMPLATE = "DELETE FROM {} WHERE {} = {}";

        private static final String UPDATE_SQL_TEMPLATE = "UPDATE {} SET {} WHERE {} = {}";

        private static final String UPDATE_FIELD_SQL_TEMPLATE = "{} = {}";

        private static final String SELECT_SQL_TEMPLATE = "SELECT * FROM {} WHERE {}";

        private static final String PAGE_SQL_TEMPLATE = SELECT_SQL_TEMPLATE + " LIMIT {}, {}";

        public void save(String tableName, Map<String, Object> data) {
            this.jdbcTemplate.update(Tools.Str.format(
                    INSERT_SQL_TEMPLATE,
                    Tools.Str.toSnakeCase(tableName),
                    Tools.Coll.join(Tools.Coll.convertList(new ArrayList<>(data.keySet()), Tools.Str::toSnakeCase)),
                    Tools.Coll.join(Tools.Coll.convertList(new ArrayList<>(data.values()), value -> value instanceof String ? "'" + value + "'" : String.valueOf(value)))
            ));
        }

        public void removeById(String tableName, String id) {
            this.jdbcTemplate.update(Tools.Str.format(
                    DELETE_SQL_TEMPLATE,
                    Tools.Str.toSnakeCase(tableName),
                    "id",
                    "'" + id + "'"
            ));
        }

        public void removeBy(String tableName, String columnName, String value) {
            this.jdbcTemplate.update(Tools.Str.format(
                    DELETE_SQL_TEMPLATE,
                    Tools.Str.toSnakeCase(tableName),
                    Tools.Str.toSnakeCase(columnName),
                    value
            ));
        }

        public void updateById(String tableName, Map<String, Object> data) {
            List<String> setValues = new ArrayList<>();
            data.forEach((key, value) -> setValues.add(Tools.Str.format(UPDATE_FIELD_SQL_TEMPLATE, Tools.Str.toSnakeCase(key), value instanceof String ? "'" + value + "'" : String.valueOf(value))));
            this.jdbcTemplate.update(Tools.Str.format(
                    UPDATE_SQL_TEMPLATE,
                    Tools.Str.toSnakeCase(tableName),
                    Tools.Coll.join(setValues),
                    "id",
                    "'" + data.get("id") + "'"
            ));
        }

        public PageResult<Map<String, Object>> page(String tableName, Map<String, Object> queryParam) {
            StringBuilder condition = new StringBuilder("1 = 1");
            queryParam.forEach((key, value) -> {
                if (!"page".equals(key) && !"rows".equals(key) && null != value) {
                    condition.append(" and ").append(Tools.Str.toSnakeCase(key));
                    if (value instanceof List) {
                        condition.append(" IN(").append(Tools.Coll.join((List) value)).append(")");
                    } else {
                        condition.append(Tools.Str.format(" LIKE CONCAT('%{}%')", value));
                    }
                }
            });
            Long count = this.jdbcTemplate.queryForObject("SELECT COUNT('id') FROM" + Tools.Str.toSnakeCase(tableName), Long.class);
            if (null == count) {
                return PageResult.build();
            }
            List<Map<String, Object>> queryResult = this.jdbcTemplate.queryForList(Tools.Str.format(
                    PAGE_SQL_TEMPLATE,
                    Tools.Str.toSnakeCase(tableName),
                    condition.toString(),
                    ((Integer) queryParam.get("page")) - 1,
                    queryParam.get("rows")
            ));
            return PageResult.build(count / Integer.toUnsignedLong((Integer) queryParam.get("rows")) + 1, count, Tools.Coll.traverse(queryResult, row -> {
                Map<String, Object> newRow = new HashMap<>();
                row.forEach((key, value) -> newRow.put(Tools.Str.toLowerCamelCase(key), value));
                return newRow;
            }));
        }

        public Map<String, Object> getById(String tableName, String id) {
            Map<String, Object> result = new HashMap<>();
            Map<String, Object> queryResult = this.jdbcTemplate.queryForMap(Tools.Str.format(
                    SELECT_SQL_TEMPLATE,
                    Tools.Str.toSnakeCase(tableName),
                    "id = '" + id + "'"
            ));
            queryResult.forEach((key, value) -> result.put(Tools.Str.toLowerCamelCase(key), value));
            return result;
        }

        public List<Map<String, Object>> list(String tableName) {
            List<Map<String, Object>> queryResult = this.jdbcTemplate.queryForList(Tools.Str.format(
                    SELECT_SQL_TEMPLATE,
                    Tools.Str.toSnakeCase(tableName),
                    "1 = 1"
            ));
            return Tools.Coll.traverse(queryResult, row -> {
                Map<String, Object> newRow = new HashMap<>();
                row.forEach((key, value) -> newRow.put(Tools.Str.toLowerCamelCase(key), value));
                return newRow;
            });
        }
    }
}
